﻿using System;
using System.Collections.Generic;
using System.Transactions;
using System.Web;
using log4net;
using TransactionException=NHibernate.TransactionException;

namespace RG.Common.Provider
{
    public class InternalHolder: IDisposable
    {
        public Guid Id { get; private set; }
        public TransactionScope tx { get; private set; }
        public bool IsFake { get; private set; }
        private Stack<Type> Providers { get; set; }
        private Dictionary<Type, IDAOTransaction> Sessions { get; set; }

        public InternalHolder(TransactionScope tx, bool isFake)
        {
            this.tx = tx;
            IsFake = isFake;
            Id = Guid.NewGuid();
            Providers = new Stack<Type>();
            Sessions = new Dictionary<Type, IDAOTransaction>();
        }

        public IDAOTransaction StartTransaction(IDAOProvider p)
        {
            Providers.Push(p.GetType());
            var session = IsFake ? new FakeSession() : p.StartTransaction;
            Sessions.Add(p.GetType(), session);
            return session;
        }

        public void Dispose()
        {
            foreach (var transaction in Sessions.Values)
                transaction.Dispose();
            tx.Dispose();
        }

        public IDAOTransaction GetSession(Type providerType)
        {
            IDAOTransaction result;
            if (Sessions.TryGetValue(providerType, out result))
                return result;


            var p = (IDAOProvider)providerType.GetConstructor(new Type[0]).Invoke(null);
            return StartTransaction(p);
        }

        public void Commit()
        {
            // TODO Так как FirebirdClient не умеет нормальный распределенных транзакций, то сначала Flush() чтобы облом был до первого коммита
            // Оно конечно целиком не защищает, но пока так 
            foreach (var tr in Sessions.Values)
                tr.Flush();

            while (Providers.Count > 0)
            {
                var pType = Providers.Pop();
                var tr = Sessions[pType];
                tr.Commit();
            }
            tx.Complete();
        }
    }

    /// <summary>
    /// Класс сохраняет контекст операции. Обычно это контекст потока,
    /// но для IIS-сервисов выполнение может продолжится в другом потоке,
    /// в этом случае нужно использовать класс HttpContext
    /// </summary>
    public abstract class ContextHolder<T>
        where T: class
    {
        public static T GetHolder()
        {
            T current;
            if (HttpContext.Current != null)
                current = (T)HttpContext.Current.Items[ContextName];
            else
                current = threadHolder;

            return current;
        }

        public static void SetHolder(T value)
        {
            if (GetHolder() != null)
                throw new TransactionException("Транзакция уже начата");

            DoSetHolder(value);
        }

        public static void RemoveHolder(T value)
        {
            var holder = GetHolder();
            if (!ReferenceEquals(holder, value))
                throw new TransactionException("Запутались в транзакциях");

            DoSetHolder(null);
        }

        private static void DoSetHolder(T value)
        {
            if (HttpContext.Current != null)
                HttpContext.Current.Items[ContextName] = value;
            else
                threadHolder = value;
        }

        private static string ContextName
        {
            get { return typeof(T).ToString(); }
        }

        [ThreadStatic]
        private static T threadHolder;
    }

    public class DAOTransactionHolder : ContextHolder<InternalHolder>
    {
        public static InternalHolder GetHolder(IEnumerable<IDAOProvider> Providers, bool fake, TimeSpan? ScopeTimeout)
        {
            // Не начинали ли пачку транзакций выше?
            if (GetHolder() != null)
            {
                if (ScopeTimeout != null && log.IsWarnEnabled)
                    log.Warn("Scope already started, ScopeTimeout ignored");
                return null;
            }

            // Начать транзакции в списке
            var holder = CreateHolder(Providers, fake, ScopeTimeout);

            if (log.IsDebugEnabled)
                log.DebugFormat("Transaction {0} started", holder.Id);

            SetHolder(holder);

            return holder;
        }

        public static void PutHolder(InternalHolder holder, bool commit)
        {
            if (holder == null) return;
            
            // Прибиваем если стартовали
            try
            {
                if (commit)
                {
                    if (log.IsDebugEnabled)
                        log.DebugFormat("Commiting transaction " + holder.Id);

                    holder.Commit();
                }
            }
            finally
            {
                RemoveHolder(holder);
                holder.Dispose();
            }
        }

        public static IDAOTransaction GetSession(Type providerType)
        {
            var holder = GetHolder();
            if (holder == null)
                throw new TransactionException("Нет активной транзакции");

            return holder.GetSession(providerType);
        }

        private static InternalHolder CreateHolder(IEnumerable<IDAOProvider> Providers, bool fake, TimeSpan? ScopeTimeout)
        {
            var scope = ScopeTimeout.HasValue
                            ? new TransactionScope(TransactionScopeOption.RequiresNew, ScopeTimeout.Value)
                            : new TransactionScope(TransactionScopeOption.RequiresNew);
            var holder = new InternalHolder(scope, fake);

            foreach (var p in Providers)
            {
                holder.StartTransaction(p);
            }
            return holder;
        }

        private static readonly ILog log = LogManager.GetLogger(typeof(DAOTransactionHolder));
    }
}
